/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#pragma once

#include <baja/common.hpp>
#include <baja/error.hpp>
#include <baja/assert.hpp>
#include <baja/capturable.hpp>

namespace baja {

template <class Ti> 
class observable_source
{
public:
    observable_source() : _nextCookie(0) {}

    int32 operator+=(const std::shared_ptr<Ti>& observer)
    {
        return this->addObserver(observer);
    }

    void operator-=(const int32 cookie)
    {
        return this->removeObserver(cookie);
    }

    void operator-=(const std::shared_ptr<Ti>& observer)
    {
        return this->removeObserver(observer);
    }

    int32 addObserver(const std::shared_ptr<Ti>& observer, void* context = nullptr)
    {
        BAJA_LOCK(_mutex);

        entry<Ti> e = entry<Ti>::create(++_nextCookie, observer, context);
        _observers.push_back(e);
        return e.cookie;
    }

    void removeObserver(const int32 cookie)
    {
        BAJA_LOCK(_mutex);

        auto it = std::find_if(
            _observers.begin(), 
            _observers.end(), 
            [&cookie](const entry<Ti>& e) { return e.cookie == cookie; }
        );

        if (it != _observers.end())
            _observers.erase(it);
    }

    void removeObserver(const std::shared_ptr<Ti>& observer)
    {
        BAJA_LOCK(_mutex);

        auto it = std::find_if(
            _observers.begin(), 
            _observers.end(), 
            [&observer](const entry<Ti>& e) 
            { 
                return e.observer.lock().get() == observer.get(); 
            }
        );

        if (it != _observers.end())
            _observers.erase(it);
    }

    int32 getObserverCount()
    {
        BAJA_LOCK(_mutex);

        return _observers.size();
    }

    std::shared_ptr<Ti> getObserver(int32 index, void** context = nullptr)
    {
        BAJA_LOCK(_mutex);

        entry<Ti>& e = _observers[index];
        if (context != nullptr)
        {
            *context = e.context;
        }
        return e.observer.lock();
    }

    void setCapture(const std::shared_ptr<Ti>& observer)
    {
        BAJA_LOCK(_mutex);

        _capture.set(observer);
    }

    void releaseCapture()
    {
        BAJA_LOCK(_mutex);

        _capture.release();
    }

    std::shared_ptr<Ti> getCapture(void** context = nullptr)
    {
        BAJA_LOCK(_mutex);

        if (!_capture.get())
        {
            return nullptr;
        }

        int32 index = this->getObserverIndex(_capture.get());
        BAJA_VERIFY(index >= 0);

        // retrieve the context
        return this->getObserver(index, context);
    }

    void raise(const std::function<void(const std::shared_ptr<Ti>&, void*)> code)
    {
        void* context = nullptr;
        std::shared_ptr<Ti> observer = this->getCapture(&context);
        if (observer)
        {
            code(observer, context);
            return;
        }

        std::vector<entry<Ti>> observers;
        {
            // take a snapshot of the observers...
            BAJA_LOCK(_mutex);

            observers = _observers;
        }

        std::for_each(
            observers.begin(),
            observers.end(),
            [&context, &code](const entry<Ti>& e)
        {
            std::shared_ptr<Ti> observer = e.observer.lock();
            if (observer != nullptr)
            {
                code(observer, context);
            }
        });
    }

private:

    // return -1 if the observer doesn't below to the list
    int32 getObserverIndex(const std::shared_ptr<Ti>& observer)
    {
        for (uint32 i = 0; i < _observers.size(); i++)
        {
            if (_observers[i].observer.lock().get() == observer.get())
            {
                return i;
            }
        }

        return -1;
    }

    template <class Ti> struct entry
    {
        void* context;
        int32 cookie;
        std::weak_ptr<Ti> observer;

        static entry create(const int32 cookie, const std::shared_ptr<Ti>& observer, void* context)
        {
            return entry(cookie, observer, context);
        }
    private:
        entry(const int32 cookie_, const std::weak_ptr<Ti>& observer_, void* context_ = nullptr) : 
           cookie(cookie_), observer(observer_), context(context_) {}
    };

private:
    std::recursive_mutex _mutex;
    capturable<Ti> _capture;
    std::vector<entry<Ti>> _observers;
    int32 _nextCookie;
};

} // namespace baja